// Written in the D Programming Language
/**
 * The 25th lesson in the <a href="http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=25">NeHe tutorial series</a>.
 * Originally written by Piotr Cieslak.
 *
 * Authors: Piotr Cieslak
 * 			Jeff Molofee
 *          Olli Aalto
 */
module lesson25;

import derelict.opengl.gl;
import derelict.opengl.glu;
import derelict.sdl.sdl;

import tango.stdc.stringz;
import tango.stdc.stdio;
import tango.text.Util;
import Int = tango.text.convert.Integer;
import tango.math.random.Kiss;

/// The window title
const char[] WINDOW_TITLE = "Piotr Cieslak & NeHe's Morphing Points Tutorial (D version)";

/// The main loop flag
bool running;

/// Random object
Kiss kiss;

/// X, Y & Z Rotation
GLfloat xrot = 0.0f, yrot = 0.0f, zrot = 0.0f;
/// X, Y & Z Spin Speed
GLfloat xspeed = 0.0f, yspeed = 0.0f, zspeed = 0.0f;
/// X, Y & Z Position
GLfloat cx = 0.0f, cy = 0.0f, cz = -15.0f;

/// Used To Make Sure Same Morph Key Is Not Pressed
int key = 1;
/// Step Counter And Maximum Number Of Steps
int step, steps = 200;
/// Default morph To False (Not Morphing)
bool morph;

/// Structure For 3D Points
struct Vertex
{
	// X, Y & Z Points
	float x, y, z;

	static Vertex opCall()
	{
		Vertex v;
		v.x = 0.0f;
		v.y = 0.0f;
		v.z = 0.0f;
		return v;
	}
}

/// Will Eventually Hold The Maximum Number Of Vertices
int maxver;
/// Our 4 Morphable Objects (morph 1, 2, 3 & 4)
Vertex[] morph1, morph2, morph3, morph4;
/// Helper Object, Source Object, Destination Object
Vertex[] helper, src, dest;

/// Imported data for the sphere
char[] sphereData = import("Sphere.txt");
/// Imported data for the torus
char[] torusData = import("Torus.txt");
/// Imported data for the tube
char[] tubeData = import("Tube.txt");

/**
 * Module constructor. Here we load the GL, GLU and SDL shared libraries,
 * and the initialize SDL.
 */
static this()
{
	DerelictGL.load();
	DerelictGLU.load();
	DerelictSDL.load();

	if(SDL_Init(SDL_INIT_VIDEO) < 0)
	{
		throw new Exception("Failed to initialize SDL: " ~ getSDLError());
	}

	if (SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY,
		SDL_DEFAULT_REPEAT_INTERVAL))
	{
		throw new Exception("Failed to set key repeat: " ~ getSDLError());
	}

	// Initialize the random object
	kiss = Kiss();
}

/**
 * Module destructor. SDL_Quit must be called somewhere, and as we initialized
 * it in the module constructor so the module destructor should be a suitable
 * place.
 */
static ~this()
{
	SDL_Quit();
}

/**
 * The main function. This is where the fun begins. The first order of business
 * is the check the command line arguments if the user wanted to start in
 * fullscreen mode. Then the window is created and OpenGL is initialized with
 * basic settings. Finally the the function starts the main loop which will live
 * for the duration of the application.
 *
 * Params:
 *      args = the command line arguments
 */
void main(char[][] args)
{
	bool fullScreen = false;
	if(args.length > 1)
	{
		fullScreen = args[1] == "-fullscreen";
	}

	createGLWindow(WINDOW_TITLE, 640, 480, 16, fullScreen);
	initGL();

	running = true;
	while(running)
	{
		processEvents();

		drawGLScene();

		SDL_GL_SwapBuffers();
		SDL_Delay(10);
	}
}

/**
 * Process all the pending events.
 */
void processEvents()
{
	SDL_Event event;
	while(SDL_PollEvent(&event))
	{
		switch(event.type)
		{
			case SDL_KEYUP:
				keyReleased(event.key.keysym.sym);
				break;
			case SDL_KEYDOWN:
				keyPressed(event.key.keysym.sym);
				break;
			case SDL_QUIT:
				running = false;
				break;
			default:
				break;
		}
	}
}

/**
 * Process a key released event.
 */
void keyReleased(int key)
{
	switch(key)
	{
		case SDLK_1:
			// Is 1 Pressed, key Not Equal To 1 And Morph False?
			if((key != 1) && !morph)
			{
				key = 1; // Sets key To 1 (To Prevent Pressing 1 2x In A Row)
				morph = true; // Set morph To True (Starts Morphing Process)
				dest = morph1.dup; // Destination Object To Morph To Becomes morph1
			}
			break;
		case SDLK_2:
			// Is 2 Pressed, key Not Equal To 2 And Morph False?
			if((key != 2) && !morph)
			{
				key = 2; // Sets key To 2 (To Prevent Pressing 2 2x In A Row)
				morph = true; // Set morph To True (Starts Morphing Process)
				dest = morph2.dup; // Destination Object To Morph To Becomes morph2
			}
			break;
		case SDLK_3:
			// Is 3 Pressed, key Not Equal To 3 And Morph False?
			if((key != 3) && !morph)
			{
				key = 3; // Sets key To 3 (To Prevent Pressing 3 2x In A Row)
				morph = true; // Set morph To True (Starts Morphing Process)
				dest = morph3.dup; // Destination Object To Morph To Becomes morph3
			}
			break;
		case SDLK_4:
			// Is 4 Pressed, key Not Equal To 4 And Morph False?
			if((key != 4) && !morph)
			{
				key = 4; // Sets key To 4 (To Prevent Pressing 4 2x In A Row)
				morph = true; // Set morph To True (Starts Morphing Process)
				dest = morph4.dup; // Destination Object To Morph To Becomes morph4
			}
			break;
		case SDLK_ESCAPE:
			running = false;
			break;
		default:
			break;
	}
}

/**
 * Process a key pressed event.
 */
void keyPressed(int key)
{
	switch(key)
	{
		case SDLK_PAGEUP:
			// Is Page Up Being Pressed?
			zspeed += 0.01f; // Increase zspeed
			break;

		case SDLK_PAGEDOWN:
			// Is Page Down Being Pressed?
			zspeed -= 0.01f; // Decrease zspeed
			break;

		case SDLK_DOWN:
			// Is Down Being Pressed?
			xspeed += 0.01f; // Increase xspeed
			break;

		case SDLK_UP:
			// Is Up Being Pressed?
			xspeed -= 0.01f; // Decrease xspeed
			break;

		case SDLK_RIGHT:
			// Is Right Being Pressed?
			yspeed += 0.01f; // Increase yspeed
			break;

		case SDLK_LEFT:
			// Is Left Being Pressed?
			yspeed -= 0.01f; // Decrease yspeed
			break;
		case SDLK_q:
			// Is Q Key Being Pressed?
			cz -= 0.01f; // Move Object Away From Viewer
			break;

		case SDLK_z:
			// Is Z Key Being Pressed?
			cz += 0.01f; // Move Object Towards Viewer
			break;

		case SDLK_w:
			// Is W Key Being Pressed?
			cy += 0.01f; // Move Object Up
			break;

		case SDLK_s:
			// Is S Key Being Pressed?
			cy -= 0.01f; // Move Object Down
			break;

		case SDLK_d:
			// Is D Key Being Pressed?
			cx += 0.01f; // Move Object Right
			break;

		case SDLK_a:
			// Is A Key Being Pressed?
			cx -= 0.01f; // Move Object Left
			break;

		default:
			break;
	}
}

/**
 * Calculates Movement Of Points During Morphing.
 * Params:
 *     i = the index of the current point
 */
Vertex calculate(int i)
{
	Vertex v; // Temporary Vertex Called v
	v.x = (src[i].x - dest[i].x) / steps; // v.x Value Equals Source x - Destination x Divided By Steps
	v.y = (src[i].y - dest[i].y) / steps; // v.y Value Equals Source y - Destination y Divided By Steps
	v.z = (src[i].z - dest[i].z) / steps; // v.z Value Equals Source z - Destination z Divided By Steps
	return v;
}

/**
 * Resize and initialize the OpenGL window.
 */
void resizeGLScene(GLsizei width, GLsizei height)
{
	if(height == 0)
	{
		height = 1;
	}
	// Reset The Current Viewport
	glViewport(0, 0, width, height);

	// Select The Projection Matrix
	glMatrixMode(GL_PROJECTION);

	// Reset The Projection Matrix
	glLoadIdentity();

	// Calculate The Aspect Ratio Of The Window
	gluPerspective(45.0f, cast(GLfloat) width / cast(GLfloat) height, 0.1f, 100.0f);

	// Select The Modelview Matrix
	glMatrixMode(GL_MODELVIEW);

	// Reset The Modelview Matrix
	glLoadIdentity();
}

/**
 * Initialize OpenGL.
 */
void initGL()
{
	// Set The Blending Function For Translucency
	glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	// Enables Smooth Shading
	glShadeModel(GL_SMOOTH);
	// Black Background
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	// Depth Buffer Setup
	glClearDepth(1.0f);
	// Enables Depth Testing
	glEnable(GL_DEPTH_TEST);
	// The Type Of Depth Test To Do
	glDepthFunc(GL_LESS);
	// Really Nice Perspective Calculations
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

	// Create the objects
	createObjects();
}

/**
 * Creates all the objects used.
 */
void createObjects()
{
	loadObject(sphereData, morph1);
	loadObject(torusData, morph2);
	loadObject(tubeData, morph3);

	// Set the size for morph4
	morph4 = new Vertex[486];
	// Loop Through morph4
	foreach (inout v; morph4)
	{
		// v.x Point Becomes A Random Float Value From -7 to 7
		v.x = (cast(float) (kiss.toInt() % 14000) * 0.001) - 7;
		// v.y Point Becomes A Random Float Value From -7 to 7
		v.y = (cast(float) (kiss.toInt() % 14000) * 0.001) - 7;
		// v.z Point Becomes A Random Float Value From -7 to 7
		v.z = (cast(float) (kiss.toInt() % 14000) * 0.001) - 7;
	}

	// Make the helper a duplicate of the first morph object
	helper = morph1.dup;
	
	// Source & Destination Are Set To Equal First Object (morph1)
	src = morph1.dup;
	dest = morph1.dup; 
}

/**
 * Load a single object.
 * 
 * Params:
 *     data = the object's data 
 *     obj = the object to be loaded
 */
void loadObject(char[] data, inout Vertex[] obj)
{
	// Split the data into lines
	char[][] lines = splitLines!(char)(data);
	// Split the first line so we can get the number of vertices
	char[][] numVertices = split!(char)(lines[0], " ");
	// Reserve memory for the object
	obj = new Vertex[Int.parse(numVertices[1])];

	// Loop through the lines, skip the first line
	for(int i = 1; i < lines.length; i++)
	{
		// Read the values from the line
		sscanf(lines[i].ptr, "%f%f%f", &obj[i - 1].x, &obj[i - 1].y, &obj[i - 1].z);
	}

	// Check if the maxver is smaller the the current object's length
	if(maxver < obj.length)
	{
		// Set the maxver to current object's length
		maxver = obj.length;
	}
}

/**
 * The drawing function. Now we only clear the color and depht buffers, so that
 * the window stays black.
 */
void drawGLScene()
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer
	glLoadIdentity(); // Reset The View

	glTranslatef(cx, cy, cz); // Translate The The Current Position To Start Drawing
	glRotatef(xrot, 1, 0, 0); // Rotate On The X Axis By xrot
	glRotatef(yrot, 0, 1, 0); // Rotate On The Y Axis By yrot
	glRotatef(zrot, 0, 0, 1); // Rotate On The Z Axis By zrot

	// Increase xrot,yrot & zrot by xspeed, yspeed & zspeed
	xrot += xspeed;
	yrot += yspeed;
	zrot += zspeed;

	glBegin(GL_POINTS); // Begin Drawing Points
	// Loop Through All The Verts Of helper (All Objects Have
	// The Same Amount Of Verts For Simplicity, Could Use maxver Also)
	foreach (i, inout v ; helper)
	{
		// Holds Returned Calculated Values For One Vertex
		Vertex q = morph ? calculate(i) : Vertex();
		v.x -= q.x; // Subtract q.x Units From helper.points[i].x (Move On X Axis)
		v.y -= q.y; // Subtract q.y Units From helper.points[i].y (Move On Y Axis)
		v.z -= q.z; // Subtract q.z Units From helper.points[i].z (Move On Z Axis)
		GLfloat tx = v.x; // Make Temp X Variable Equal To Helper's X Variable
		GLfloat ty = v.y; // Make Temp Y Variable Equal To Helper's Y Variable
		GLfloat tz = v.z; // Make Temp Z Variable Equal To Helper's Z Variable

		glColor3f(0.0f, 1.0f, 1.0f); // Set Color To A Bright Shade Of Off Blue
		glVertex3f(tx, ty, tz); // Draw A Point At The Current Temp Values (Vertex)
		
		// Calculate Two Positions Ahead
		tx -= 2 * q.x;
		ty -= 2 * q.y;
		tz -= 2 * q.z; 
		glColor3f(0.0f, 0.5f, 1.0f); // Darken Color A Bit
		glVertex3f(tx, ty, tz); // Draw A Second Point At The Newly Calculate Position
		
		// Calculate Two More Positions Ahead
		tx -= 2 * q.x;
		ty -= 2 * q.y;
		tz -= 2 * q.z;		
		glColor3f(0.0f, 0.0f, 1.0f); // Set Color To A Very Dark Blue
		glVertex3f(tx, ty, tz); // Draw A Third Point At The Second New Position
								// This Creates A Ghostly Tail As Points Move
	}
	glEnd(); // Done Drawing Points

	// If We're Morphing And We Haven't Gone Through All 200 Steps Increase Our Step Counter
	// Otherwise Set Morphing To False, Make Source=Destination And Set The Step Counter Back To Zero.
	if(morph && step <= steps)
	{
		step++;
	}
	else
	{
		morph = false;
		src = dest.dup;
		step = 0;
	}
}

/**
 * Initializes and opens the SDL window.
 */
void createGLWindow(char[] title, int width, int height, int bits, bool fullScreen)
{
	// Set the OpenGL attributes
	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 6);
	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);

	// Set the window title
	SDL_WM_SetCaption(toStringz(title), null);

	// Note the SDL_DOUBLEBUF flag is not required to enable double
	// buffering when setting an OpenGL video mode.
	// Double buffering is enabled or disabled using the
	// SDL_GL_DOUBLEBUFFER attribute. (See above.)
	int mode = SDL_OPENGL;
	if(fullScreen)
	{
		mode |= SDL_FULLSCREEN;
	}
	// Now open a SDL OpenGL window with the given parameters
	if(SDL_SetVideoMode(width, height, bits, mode) is null)
	{
		throw new Exception("Failed to open OpenGL window: " ~ getSDLError());
	}

	resizeGLScene(width, height);
}

/**
 * Get the SDL error as a D string.
 *
 * Returns: A D string containing the current SDL error.
 */
char[] getSDLError()
{
	return fromStringz(SDL_GetError());
}
